package org.geebis.powerbrake;

import org.geebis.powerbrake.parser.ParseException;
import org.geebis.powerbrake.parser.ScriptParser;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This is the main PowerBrake class. It contains the <code>main(String[])</code> method.
 */
public class PowerBrake {
    public static void main(String[] args) {
        try {
            Config config = PowerBrake.parseArgs(args);

            File hb = config.getHandbrake();
            File ap = config.getAtomicParsley();
            if (!hb.exists()) {
                System.out.println("Could not find handbrake at: \"" + hb.getAbsolutePath() + "\".");
                System.exit(1);
                return;
            }
            if (!ap.exists()) {
                System.out.println("Could not find atomic parsley at: \"" + ap.getAbsolutePath() + "\".");
                System.exit(1);
                return;
            }

            File outputDirectory = config.getOutputDirectory();
            if (!outputDirectory.exists()) {
                System.out.println("Could not find the output directory: \"" + outputDirectory.getAbsolutePath() + "\".");
                System.exit(1);
                return;
            }

            File sourceBaseDirectory = config.getSourceBase();
            if (!sourceBaseDirectory.exists()) {
                System.out.println("Could not find the source directory: \"" + sourceBaseDirectory.getAbsolutePath() + "\".");
                System.exit(1);
                return;
            }

            boolean tagOnly = config.isTagOnly();
            boolean encodeOnly = config.isEncodeOnly();

            if (tagOnly && encodeOnly) {
                System.out.println("You cannot specify both the --tag-only (-t) and --encode-only (-e) switches.");
                System.exit(1);
                return;
            }

            if (tagOnly && config.isResume()) {
                System.out.println("You cannot specify both the --tag-only (-t) and --resume switches.");
                System.exit(1);
                return;
            }

            String script = config.getScript();
            InputStream scriptStream;
            if (script.equals("-")) {
                scriptStream = System.in;
            } else {
                try {
                    scriptStream = new FileInputStream(new File(script));
                } catch (FileNotFoundException e) {
                    System.err.println("Could not find the script file: " + script + ".");
                    System.exit(1);
                    return;
                }
            }

            List<Job> jobs = ScriptParser.parse(scriptStream);

            checkJobs(sourceBaseDirectory, outputDirectory, jobs, config);

            for (Job job : jobs) {
                File videoTsDirectory = new File(sourceBaseDirectory, job.getSourceVideoTs());
                File mp4File = new File(outputDirectory, job.getOutputFileName());

                if (mp4File.exists() && config.isResume()) {
                    continue;
                }

                boolean shouldEncode = !tagOnly;
                boolean shouldTag = !encodeOnly;

                if (shouldEncode) {
                    encode(hb, videoTsDirectory, mp4File, job);
                }

                if (shouldTag) {
                    tag(ap, mp4File, job);
                }
            }
        } catch (ParseException e) {
            throw new RuntimeException("There was an error parsing the script.", e);
        }
    }

    private static void checkJobs(File sourceBaseDirectory, File outputDirectory, List<Job> jobs, Config config) {
        for (Job job : jobs) {
            File videoTsDirectory = new File(sourceBaseDirectory, job.getSourceVideoTs());
            if (!videoTsDirectory.exists() && !config.isTagOnly()) {
                System.out.println(String.format("Could not find the VIDEO_TS directory \"%s\" for job \"%s\".", videoTsDirectory, job.getTitle()));
                System.exit(1);
                return;
            }

            File mp4File = new File(outputDirectory, job.getOutputFileName());
            if (mp4File.exists() && !(config.isForce() || config.isResume() || config.isTagOnly())) {
                System.out.println(String.format("Output file \"%s\" already exists. Specify the --force flag to overwrite existing files, or use the --resume option.", mp4File.getAbsolutePath()));
                System.exit(1);
                return;
            }
        }
    }

    private static void encode(File handbrake, File videoTsDirectory, File mp4File, Job job) {
        List<String> args = new ArrayList<String>();

        args.add(handbrake.getAbsolutePath());

        args.add("-i");
        args.add(videoTsDirectory.getAbsolutePath());

        args.add("-o");
        args.add(mp4File.getAbsolutePath());

        Pattern argsPattern = Pattern.compile("(?:([^\"\\s]+))|(?:\"([^\"]*)\")");

        Matcher m = argsPattern.matcher(job.getHandbrakeOptions());
        while (m.find()) {
            if (m.groupCount() > 0) {
                if (m.group(1) == null) {
                    args.add(m.group(2));
                } else {
                    args.add(m.group(1));
                }
            }
        }

        try {
            ProcessBuilder builder = new ProcessBuilder(args);
            Process handbrakeProcess = builder.start();

            BufferedReader in = new BufferedReader(new InputStreamReader(handbrakeProcess.getInputStream()));
            String line;

            System.out.println();
            Pattern hbOutputPattern = Pattern.compile("^Encoding.*$");

            StringBuffer argsString = new StringBuffer();
            for (String arg : args) {
                if (arg.indexOf(" ") > 0) {
                    arg = "\"" + arg + "\"";
                }

                argsString.append(arg);
                if (arg != args.get(args.size() - 1)) {
                    argsString.append(" ");
                }
            }

            System.out.println("Options: " + argsString.toString());

            while ((line = in.readLine()) != null) {
                m = hbOutputPattern.matcher(line);
                if (m.matches()) {
                    System.out.print("\r" + line);
                }
            }
            System.out.println();
            in.close();

            try {
                handbrakeProcess.waitFor();
            } catch (InterruptedException e) {
                throw new RuntimeException("Encoding was interrupted...", e);
            }
        } catch (IOException e) {
            throw new RuntimeException("There was an error executing HandBrakeCLI.", e);
        }
    }

    private static void tag(File atomicParsley, File mp4File, Job job) {
        List<String> args = new ArrayList<String>();
        args.add(atomicParsley.getAbsolutePath());

        File tmpMp4File = new File(mp4File.getAbsolutePath() + ".tmp.mp4");

        if (!mp4File.exists()) {
            System.out.println("Could not find the mp4 file: \"" + mp4File.getAbsolutePath() + "\"");
            System.exit(1);
            return;
        }

        args.add(mp4File.getAbsolutePath());

        args.add("--output");
        args.add(tmpMp4File.getAbsolutePath());

        if (job.getTitle() != null) {
            args.add("--title");
            args.add(job.getTitle());
        }
        if (job.getDescription() != null) {
            args.add("--description");
            args.add(job.getDescription());

            args.add("--comment");
            args.add(job.getDescription());
        }
        if (job.getArtist() != null) {
            args.add("--artist");
            args.add(job.getArtist());
        }
        if (job.getAlbum() != null) {
            args.add("--album");
            args.add(job.getAlbum());
        }
        if (job.getNetwork() != null) {
            args.add("--TVNetwork");
            args.add(job.getNetwork());
        }
        if (job.getSeason() != null) {
            args.add("--TVSeasonNum");
            args.add(job.getSeason().toString());
        }
        if (job.getShow() != null) {
            args.add("--TVShowName");
            args.add(job.getShow());
        }
        if (job.getEpisodeNumber() != null) {
            args.add("--TVEpisodeNum");
            args.add(job.getEpisodeNumber().toString());
        }
        if (job.getEpisodeId() != null) {
            args.add("--TVEpisode");
            args.add(job.getEpisodeId());
        }
        if (job.getExplicit() != null && job.getExplicit()) {
            args.add("--advisory");
            args.add("explicit");
        }
        if (job.getGenre() != null) {
            args.add("--genre");
            args.add(job.getGenre());
        } else {
            String genre = null;
            switch (job.getKind()) {
                case TV_SHOW:
                    genre = "TV Shows";
                    break;
            }

            if (genre != null) {
                args.add("--genre");
                args.add(genre);
            }
        }
        if (job.getYear() != null) {
            args.add("--year");
            args.add(job.getYear().toString());
        }
        if (job.getTrack() != null) {
            args.add("--tracknum");
            if (job.getTrackCount() != null) {
                args.add(job.getTrack().toString() + "/" + job.getTrackCount().toString());
            } else {
                args.add(job.getTrack().toString());
            }
        }
        if (job.getDisc() != null) {
            args.add("--disk");
            if (job.getDiscCount() != null) {
                args.add(job.getDisc().toString() + "/" + job.getDiscCount().toString());
            } else {
                args.add(job.getDisc().toString());
            }
        }

        int kindNumber = job.getKind().getNumericValue();
        args.add("--stik");
        args.add(String.format("value=%s", kindNumber));

        try {
            Process atomicParsleyProcess = Runtime.getRuntime().exec(args.toArray(new String[]{}));

            // Get the input stream and read from it
            BufferedReader in = new BufferedReader(new InputStreamReader(atomicParsleyProcess.getInputStream()));
            String line;
            Pattern p = Pattern.compile("[^\\d]*(\\d*%)[^\\d]*");
            System.out.println("Tagging file...");
            while ((line = in.readLine()) != null) {
                Matcher m = p.matcher(line);
                if (m.matches()) {
                    if (m.groupCount() > 0) {
                        System.out.print("\rTagging: " + m.group(1));
                    }
                }
            }
            System.out.println();
            in.close();

            try {
                atomicParsleyProcess.waitFor();
            } catch (InterruptedException e) {
                throw new RuntimeException("Tagging process cancelled...", e);
            }

            System.out.println("Overwriting old file...");
            tmpMp4File.renameTo(mp4File);

        } catch (IOException e) {
            throw new RuntimeException("There was a problem executing AtomicParsley.", e);
        }
    }

    private static Config parseArgs(String[] args) {
        Config config = new Config();
        CmdLineParser parser = new CmdLineParser(config);

        parser.setUsageWidth(80);

        try {
            parser.parseArgument(args);
        } catch (CmdLineException e) {
            parser.printUsage(System.err);
            System.exit(1);
            return null;
        }

        if (config.isHelp()) {
            parser.printUsage(System.out);
            System.exit(1);
            return null;
        }

        return config;
    }

    private static class Config {
        @Option(name = "-d", usage = "specifies the source base directory", metaVar = "DIRECTORY")
        private File sourceBase = new File(".");
        @Option(name = "-o", usage = "specifies the output directory", metaVar = "DIRECTORY")
        private File outputDirectory = new File(".");
        @Option(name = "-f", usage = "specifies the script file, or - to read from stdin", metaVar = "FILE")
        private String script = "-";
        @Option(name = "-t", usage = "do not encode, just add mp4 tags")
        private boolean tagOnly = false;
        @Option(name = "-e", usage = "do not add mp4 tags, just encode")
        private boolean encodeOnly = false;
        @Option(name = "--handbrake", required = true, usage = "the path to the handbrake command line program")
        private File handbrake;
        @Option(name = "--atomic-parsley", required = true, usage = "the path to the AtomicParsley command line program")
        private File atomicParsley;
        @Option(name = "--force", usage = "overwrite existing files")
        private boolean force = false;
        @Option(name = "--resume", usage = "resume from a previous run")
        private boolean resume = false;
        @Option(name = "--help")
        private boolean help = false;
        @Option(name = "--dry")
        private boolean dry = false;

        public File getSourceBase() {
            return sourceBase;
        }

        public void setSourceBase(File sourceBase) {
            this.sourceBase = sourceBase;
        }

        public File getOutputDirectory() {
            return outputDirectory;
        }

        public void setOutputDirectory(File outputDirectory) {
            this.outputDirectory = outputDirectory;
        }

        public String getScript() {
            return script;
        }

        public void setScript(String script) {
            this.script = script;
        }

        public boolean isTagOnly() {
            return tagOnly;
        }

        public void setTagOnly(boolean tagOnly) {
            this.tagOnly = tagOnly;
        }

        public boolean isEncodeOnly() {
            return encodeOnly;
        }

        public void setEncodeOnly(boolean encodeOnly) {
            this.encodeOnly = encodeOnly;
        }

        public File getHandbrake() {
            return handbrake;
        }

        public void setHandbrake(File handbrake) {
            this.handbrake = handbrake;
        }

        public File getAtomicParsley() {
            return atomicParsley;
        }

        public void setAtomicParsley(File atomicParsley) {
            this.atomicParsley = atomicParsley;
        }

        public boolean isForce() {
            return force;
        }

        public void setForce(boolean force) {
            this.force = force;
        }

        public boolean isResume() {
            return resume;
        }

        public void setResume(boolean resume) {
            this.resume = resume;
        }

        public boolean isHelp() {
            return help;
        }

        public void setHelp(boolean help) {
            this.help = help;
        }

        public boolean isDry() {
            return dry;
        }

        public void setDry(boolean dry) {
            this.dry = dry;
        }
    }
}
